1 /*
2  * Hunt - a framework for web and console application based on Collie using Dlang development
3  *
4  * Copyright (C) 2015-2017  Shanghai Putao Technology Co., Ltd
5  *
6  * Developer: HuntLabs
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11  
12 module hunt.http.cookie;
13 
14 import std.regex : regex, Regex;
15 import std.string;
16 import std.conv;
17 
18 static import std.algorithm;
19 import core.stdc.stdlib;
20 import std.stdio;
21 
22 // XXX VOLVER A PROBAR, HE CAMBIADO TODO LO DE REGEX!
23 
24 // Inspired by Python's Cookie.py but Cookie objects are a little different; this 
25 // class only stores one cookie per object, not several like the Python version.
26 // Use parse_cookie_header to get a Cookie[] with all the cookies found in a 
27 // string
28 
29 class CookieException : Exception
30 {
31     this(string msg, string file = __FILE__, size_t line = __LINE__)
32     {
33         super(msg, file, line);
34     }
35 }
36 
37 // Used for encoded cookie values matching
38 Regex!char _OctalPatt;
39 Regex!char _QuotePatt;
40 string[string] RESERVED_PARAMS = null;
41 
42 static this()
43 {
44     _OctalPatt = regex(r"\\[0-3][0-7][0-7]");
45     _QuotePatt = regex(r"[\\].");
46 
47     RESERVED_PARAMS = ["expires" : "expires", "path" : "Path",
48         "comment" : "Comment", "domain" : "Domain", "max-age" : "Max-Age",
49         "secure" : "secure", "httponly" : "httponly", "version" : "Version"];
50 }
51 
52 // Chars not needing quotation (fake set for fast lookup)
53 enum _legalchars = ['a' : 0, 'b' : 0, 'c' : 0, 'd' : 0, 'e' : 0, 'f' : 0, 'g' : 0,
54         'h' : 0, 'i' : 0, 'j' : 0, 'k' : 0, 'l' : 0, 'm' : 0, 'n' : 0, 'o' : 0,
55         'p' : 0, 'q' : 0, 'r' : 0, 's' : 0, 't' : 0, 'u' : 0, 'v' : 0, 'w' : 0,
56         'x' : 0, 'y' : 0, 'z' : 0, 'A' : 0, 'B' : 0, 'C' : 0, 'D' : 0, 'E' : 0,
57         'F' : 0, 'G' : 0, 'H' : 0, 'I' : 0, 'J' : 0, 'K' : 0, 'L' : 0, 'M' : 0,
58         'N' : 0, 'O' : 0, 'P' : 0, 'Q' : 0, 'R' : 0, 'S' : 0, 'T' : 0, 'U' : 0,
59         'V' : 0, 'W' : 0, 'X' : 0, 'Y' : 0, 'Z' : 0, '0' : 0, '1' : 0, '2' : 0,
60         '3' : 0, '4' : 0, '5' : 0, '6' : 0, '7' : 0, '8' : 0, '9' : 0, '!' : 0,
61         '#' : 0, '$' : 0, '%' : 0, '&' : 0, '\'' : 0, '*' : 0, '+' : 0, '-' : 0,
62         '.' : 0, '^' : 0, '_' : 0, '`' : 0, '|' : 0, '~' : 0];
63 
64 // Hash for quickly translating chars not in _legalchars
65 // The "," and ";" are encoded for compatibility with some 
66 // Safari & Explorer versions
67 enum _cookiechars = [
68         std.conv.octal!0 : "\\000", std.conv.octal!1 : "\\001",
69         std.conv.octal!2 : "\\002", std.conv.octal!3 : "\\003",
70         std.conv.octal!4 : "\\004", std.conv.octal!5 : "\\005",
71         std.conv.octal!6 : "\\006", std.conv.octal!7 : "\\007",
72         std.conv.octal!10 : "\\010", std.conv.octal!11 : "\\011",
73         std.conv.octal!12 : "\\012", std.conv.octal!13 : "\\013",
74         std.conv.octal!14 : "\\014", std.conv.octal!15 : "\\015",
75         std.conv.octal!16 : "\\016", std.conv.octal!17 : "\\017",
76         std.conv.octal!20 : "\\020", std.conv.octal!21 : "\\021",
77         std.conv.octal!22 : "\\022", std.conv.octal!23 : "\\023",
78         std.conv.octal!24 : "\\024", std.conv.octal!25 : "\\025",
79         std.conv.octal!26 : "\\026", std.conv.octal!27 : "\\027",
80         std.conv.octal!30 : "\\030", std.conv.octal!31 : "\\031",
81         std.conv.octal!32 : "\\032", std.conv.octal!33 : "\\033",
82         std.conv.octal!34 : "\\034", std.conv.octal!35 : "\\035",
83         std.conv.octal!36 : "\\036", std.conv.octal!37 : "\\037",
84 
85         std.conv.octal!54 : "\\054", std.conv.octal!73 : "\\073",
86 
87         std.conv.octal!177 : "\\177", std.conv.octal!200 : "\\200",
88         std.conv.octal!201 : "\\201", std.conv.octal!202 : "\\202",
89         std.conv.octal!203 : "\\203", std.conv.octal!204 : "\\204",
90         std.conv.octal!205 : "\\205", std.conv.octal!206 : "\\206",
91         std.conv.octal!207 : "\\207", std.conv.octal!210 : "\\210",
92         std.conv.octal!211 : "\\211", std.conv.octal!212 : "\\212",
93         std.conv.octal!213 : "\\213", std.conv.octal!214 : "\\214",
94         std.conv.octal!215 : "\\215", std.conv.octal!216 : "\\216",
95         std.conv.octal!217 : "\\217", std.conv.octal!220 : "\\220",
96         std.conv.octal!221 : "\\221", std.conv.octal!222 : "\\222",
97         std.conv.octal!223 : "\\223", std.conv.octal!224 : "\\224",
98         std.conv.octal!225 : "\\225", std.conv.octal!226 : "\\226",
99         std.conv.octal!227 : "\\227", std.conv.octal!230 : "\\230",
100         std.conv.octal!231 : "\\231", std.conv.octal!232 : "\\232",
101         std.conv.octal!233 : "\\233", std.conv.octal!234 : "\\234",
102         std.conv.octal!235 : "\\235", std.conv.octal!236 : "\\236",
103         std.conv.octal!237 : "\\237", std.conv.octal!240 : "\\240",
104         std.conv.octal!241 : "\\241", std.conv.octal!242 : "\\242",
105         std.conv.octal!243 : "\\243", std.conv.octal!244 : "\\244",
106         std.conv.octal!245 : "\\245", std.conv.octal!246 : "\\246",
107         std.conv.octal!247 : "\\247", std.conv.octal!250 : "\\250",
108         std.conv.octal!251 : "\\251", std.conv.octal!252 : "\\252",
109         std.conv.octal!253 : "\\253", std.conv.octal!254 : "\\254",
110         std.conv.octal!255 : "\\255", std.conv.octal!256 : "\\256",
111         std.conv.octal!257 : "\\257", std.conv.octal!260 : "\\260",
112         std.conv.octal!261 : "\\261", std.conv.octal!262 : "\\262",
113         std.conv.octal!263 : "\\263", std.conv.octal!264 : "\\264",
114         std.conv.octal!265 : "\\265", std.conv.octal!266 : "\\266",
115         std.conv.octal!267 : "\\267", std.conv.octal!270 : "\\270",
116         std.conv.octal!271 : "\\271", std.conv.octal!272 : "\\272",
117         std.conv.octal!273 : "\\273", std.conv.octal!274 : "\\274",
118         std.conv.octal!275 : "\\275", std.conv.octal!276 : "\\276",
119         std.conv.octal!277 : "\\277", std.conv.octal!300 : "\\300",
120         std.conv.octal!301 : "\\301", std.conv.octal!302 : "\\302",
121         std.conv.octal!303 : "\\303", std.conv.octal!304 : "\\304",
122         std.conv.octal!305 : "\\305", std.conv.octal!306 : "\\306",
123         std.conv.octal!307 : "\\307", std.conv.octal!310 : "\\310",
124         std.conv.octal!311 : "\\311", std.conv.octal!312 : "\\312",
125         std.conv.octal!313 : "\\313", std.conv.octal!314 : "\\314",
126         std.conv.octal!315 : "\\315", std.conv.octal!316 : "\\316",
127         std.conv.octal!317 : "\\317", std.conv.octal!320 : "\\320",
128         std.conv.octal!321 : "\\321", std.conv.octal!322 : "\\322",
129         std.conv.octal!323 : "\\323", std.conv.octal!324 : "\\324",
130         std.conv.octal!325 : "\\325", std.conv.octal!326 : "\\326",
131         std.conv.octal!327 : "\\327", std.conv.octal!330 : "\\330",
132         std.conv.octal!331 : "\\331", std.conv.octal!332 : "\\332",
133         std.conv.octal!333 : "\\333", std.conv.octal!334 : "\\334",
134         std.conv.octal!335 : "\\335", std.conv.octal!336 : "\\336",
135         std.conv.octal!337 : "\\337", std.conv.octal!340 : "\\340",
136         std.conv.octal!341 : "\\341", std.conv.octal!342 : "\\342",
137         std.conv.octal!343 : "\\343", std.conv.octal!344 : "\\344",
138         std.conv.octal!345 : "\\345", std.conv.octal!346 : "\\346",
139         std.conv.octal!347 : "\\347", std.conv.octal!350 : "\\350",
140         std.conv.octal!351 : "\\351", std.conv.octal!352 : "\\352",
141         std.conv.octal!353 : "\\353", std.conv.octal!354 : "\\354",
142         std.conv.octal!355 : "\\355", std.conv.octal!356 : "\\356",
143         std.conv.octal!357 : "\\357", std.conv.octal!360 : "\\360",
144         std.conv.octal!361 : "\\361", std.conv.octal!362 : "\\362",
145         std.conv.octal!363 : "\\363", std.conv.octal!364 : "\\364",
146         std.conv.octal!365 : "\\365", std.conv.octal!366 : "\\366",
147         std.conv.octal!367 : "\\367", std.conv.octal!370 : "\\370",
148         std.conv.octal!371 : "\\371", std.conv.octal!372 : "\\372",
149         std.conv.octal!373 : "\\373", std.conv.octal!374 : "\\374",
150         std.conv.octal!375 : "\\375", std.conv.octal!376 : "\\376",
151         std.conv.octal!377 : "\\377",
152     ];
153 
154 bool has_legal_chars(string s)
155 {
156     foreach (c; s)
157     {
158         if (c !in _legalchars)
159             return false;
160     }
161 
162     return true;
163 }
164 
165 string cookie_quote(string input)
166 {
167     char[] result = new char[input.length * 4];
168     uint lastid = 0;
169     bool usedspecial = false;
170 
171     foreach (c; input)
172     {
173 
174         if (c !in _legalchars)
175         {
176             // Not legal char
177             usedspecial = true;
178 
179             if (cast(uint) c in _cookiechars)
180             {
181                 // We got encoding for it
182                 result[lastid .. lastid + 4] = _cookiechars[cast(uint) c];
183                 lastid += 4;
184             }
185             else
186             {
187                 // Not in legalchars, not in the cookiechars either... just append it, 
188                 // but the string is already marked as "special" so it will be quoted
189                 result[lastid] = c;
190                 ++lastid;
191             }
192 
193         }
194         else
195         {
196             result[lastid] = c;
197             ++lastid;
198         }
199     }
200 
201     result.length = lastid;
202 
203     // Put quotes around the string if we used some encoded character
204     return usedspecial ? '"' ~ to!string(result) ~ '"' : to!string(result);
205 }
206 
207 // XXX Optimize this, use a string Appender or a char[], etc...
208 string cookie_unquote(string quoted_value)
209 {
210     string result = "";
211 
212     // If there aren't any doublequotes, then there can't special chars. See RFC 2109
213     if (quoted_value.length < 2)
214         return quoted_value;
215 
216     //if (quoted_value[0] != '"' || quoted_value[$-1] != '"')
217     //return quoted_value;
218 
219     // Remove the "..."
220     string unquoted = quoted_value; //[1..$-1];
221 
222     // Check for special chars \012 => \n, \" => "
223     size_t i = 0;
224     auto n = unquoted.length;
225     size_t Omatch, Qmatch;
226     size_t j, k;
227 
228     while (i >= 0 && i < n)
229     {
230         import std.regex;
231 
232         auto ocaptures = match(unquoted[i .. $], _OctalPatt).captures;
233         if (ocaptures.length == 0)
234             Omatch = -1;
235         else
236             Omatch = std..string.indexOf(unquoted[i .. $], ocaptures[0]);
237 
238         auto qcaptures = match(unquoted[i .. $], _QuotePatt).captures;
239         if (qcaptures.length == 0)
240             Qmatch = -1;
241         else
242             Qmatch = std..string.indexOf(unquoted[i .. $], qcaptures[0]);
243 
244         if (Omatch == -1 && Qmatch == -1)
245         { // Neither matched
246             result ~= unquoted[i .. $];
247             break;
248         }
249 
250         j = -1;
251         k = -1;
252         if (Omatch != -1)
253             j = Omatch + i;
254         if (Qmatch != -1)
255             k = Qmatch + i;
256 
257         if (Qmatch != -1 && ((Omatch == -1) || (k < j))) // QuotePatt matched
258         {
259             result ~= unquoted[i .. k] ~ unquoted[k + 1];
260             i = k + 2;
261         }
262         else // OctalPatt matched
263         {
264             result ~= unquoted[i .. j];
265             result ~= cast(char) strtoul(toStringz(unquoted[j + 1 .. j + 4]), null,
266                 8);
267             i = j + 4;
268         }
269     }
270 
271     return result;
272 }
273 
274 // XXX: Capture possible exceptions
275 // Convert a (possibly encoded) client "Cookie: " HTTP header into an
276 // list of Cookie objects. This function expects the "Cookie: " or 
277 // "Set-Cookie: " header name to be already removed
278 Cookie[string] parseCookie(string header)
279 {
280     /// if cookie string is not null
281     if (header is null)
282         return null;
283 
284     /// parse the cookies
285     Cookie[string] result;
286 
287     string[] cookie_parts = header.split(";");
288 
289     foreach (idx, part; cookie_parts)
290     {
291         auto cookie = new Cookie();
292         string[] keyvalue = part.split("=");
293         if (keyvalue.length != 2) // WTF!?
294             continue;
295 
296         string key = keyvalue[0].strip;
297 
298         if (key[0] == '$')
299             continue;
300 
301         string quoted_value = keyvalue[1];
302 
303         string lkey = toLower(key);
304 
305         if (lkey in RESERVED_PARAMS)
306         {
307             // Ignore if we've not set the name yet
308             if (!cookie.is_name_set)
309                 continue;
310 
311             cookie.set(lkey, cookie_unquote(quoted_value));
312 
313             // Add the cookie if this is the last token
314             if (idx == cookie_parts.length - 1)
315             {
316                 result[lkey] = cookie;
317             }
318         }
319 
320         // It is a name-value, not a reserved param
321         else
322         {
323             cookie.set(key, cookie_unquote(quoted_value));
324             result[key] = cookie;
325         }
326     }
327 
328     return result;
329 }
330 
331 // =================================
332 // Cookie class
333 // =================================
334 class Cookie
335 {
336     enum DEFAULT_HEADER = "Set-Cookie: ";
337 
338     static bool is_reserved_key(string key)
339     {
340         return (toLower(key) in RESERVED_PARAMS) != null;
341     }
342 
343     this(string cname, string cvalue, string[string] cparams)
344     {
345         this(cname, cvalue);
346         params(cparams);
347 
348     }
349 
350     this(string cname, string cvalue)
351     {
352         set(cname, cvalue);
353         this();
354     }
355 
356     this()
357     {
358         initialize_cookieparams();
359     }
360 
361     void set(string name, string value)
362     in
363     {
364         assert(name != null);
365         //assert(value != null);
366     }
367     body
368     {
369         string lname = toLower(name);
370         if (lname in RESERVED_PARAMS)
371         {
372             _cookieparams[lname] = value;
373         }
374         else
375         {
376             // Check that all the chars in the name are legal
377             if (!has_legal_chars(name))
378                 throw new CookieException("Illegal name '" ~ name ~ "' has ilegal chars");
379 
380             _name = name;
381             _value = value;
382             _quoted_value = cookie_quote(value);
383         }
384     }
385 
386     @property string name()
387     {
388         if (_name is null)
389             throw new CookieException("Cookie name not set");
390         return _name;
391     }
392 
393     @property void name(string newname)
394     {
395         if (!has_legal_chars(name))
396             throw new CookieException("The name '" ~ name ~ "' has ilegal chars");
397 
398         _name = newname;
399     }
400 
401     @property string value()
402     {
403         if (_value is null)
404             throw new CookieException("Cookie value not set");
405         return _value;
406     }
407 
408     @property void value(string newvalue)
409     {
410         _value = newvalue;
411         _quoted_value = cookie_quote(_value);
412     }
413 
414     @property string quoted_value()
415     {
416         if (_quoted_value is null)
417             throw new CookieException("Cookie quoted_value not set");
418         return _quoted_value;
419     }
420 
421     @property void quoted_value(string newvalue)
422     {
423         _quoted_value = newvalue;
424         _value = cookie_unquote(_quoted_value);
425     }
426 
427     @property string[string] params()
428     {
429         return _cookieparams;
430     }
431 
432     @property void params(string[string] newparams)
433     {
434         // Join the dicts
435         foreach (key, value; newparams)
436         {
437             if (!has_legal_chars(key))
438                 throw new CookieException("The key '" ~ key ~ "' has ilegal chars");
439 
440             string lkey = toLower(key);
441             if (lkey in RESERVED_PARAMS)
442                 _cookieparams[lkey] = value;
443             else
444                 throw new CookieException("Wrong cookie parameter '" ~ key ~ "'");
445         }
446     }
447 
448     @property bool is_name_set()
449     {
450         return !(_name is null);
451     }
452 
453     @property bool is_value_set()
454     {
455         return !(_value is null);
456     }
457 
458     string get(const string key)
459     {
460         auto res = get(key, null);
461 
462         return res;
463     }
464 
465     string get(const string key, const string _default)
466     {
467         if (key == _name)
468             return _value;
469 
470         string lkey = toLower(key);
471         if (lkey in _cookieparams)
472             return _cookieparams[lkey];
473 
474         return _default;
475     }
476 
477     void setkey(string key, string _value)
478     {
479         string lkey = toLower(key);
480         if (lkey !in RESERVED_PARAMS && key != _name)
481             throw new CookieException(
482                 "Wrong cookie index '" ~ key ~ "'. Use 'name', 'value', 'quoted_value' or valid cookie parameter (see RFC 2109)");
483 
484         if (key == _name)
485             name(_value);
486         else
487             _cookieparams[lkey] = _value;
488     }
489 
490     string output(string[] attrs, string header)
491     {
492         string result = header ~ _name ~ "=" ~ _quoted_value;
493         auto paramkeys = _cookieparams.keys;
494 
495         foreach (param, value; _cookieparams)
496         {
497             // Add the cooieparam only if is in the user specified attrs and is not empty
498             if (std.algorithm.countUntil(attrs, param) != -1 && value.length > 0)
499                 result ~= ";" ~ param ~ "=" ~ value;
500         }
501         return result;
502     }
503 
504     string output(string header)
505     {
506         return output(_cookieparams.keys, header);
507     }
508 
509     string output()
510     {
511         return output(_cookieparams.keys, DEFAULT_HEADER);
512     }
513 
514     override string toString()
515     {
516         return output();
517     }
518 
519 private:
520     string _name = null;
521     string _quoted_value = null;
522     string _value = null;
523     string _decodedvalue = null;
524     string[string] _cookieparams = null;
525 
526 protected:
527     void initialize_cookieparams()
528     {
529         foreach (key, value; RESERVED_PARAMS)
530             _cookieparams[key] = "";
531     }
532 
533 }
534 
535 unittest
536 {
537     auto cookies = parseCookie("PHPSESSID=dh5vvosj68hv1raprertnku6s7; LBN=node2; Hm_lvt_9e6c6312b8b64e7e38b0b84c12642b96=1461739077,1461897406,1462760128,1462760222; Hm_lpvt_9e6c6312b8b64e7e38b0b84c12642b96=1463122691; __utmt=1; __utma=233165215.1997191855.1458546658.1463106445.1463122691.5; __utmb=233165215.1.10.1463122691; __utmc=233165215; __utmz=233165215.1458546658.1.1.utmcsr=account.start.wang|utmccn=(referral)|utmcmd=referral|utmcct=/register");
538     import std.experimental.logger;
539 
540     assert(cookies.get("PHPSESSID", null).value == "dh5vvosj68hv1raprertnku6s7");
541     assert(cookies["LBN"].value == "node2");
542 }
543 
544 unittest
545 {
546     //generate cookie
547     auto cookie = new Cookie("PHPSESSID", "dh5vvosj68hv1raprertnku6s7");
548     //assert();
549     import std.experimental.logger;
550 
551     assert(cookie.output == "Set-Cookie: PHPSESSID=dh5vvosj68hv1raprertnku6s7");
552     cookie.params = ["expires" : "Fri, 13 May 2016 17:44:17 GMT", "path" : "/",
553         "domain" : "putao.com", "secure" : "true", "httponly" : "false"];
554 
555     assert( cookie.output == "Set-Cookie: PHPSESSID=dh5vvosj68hv1raprertnku6s7;expires=Fri, 13 May 2016 17:44:17 GMT;domain=putao.com;path=/;secure=true;httponly=false");
556 
557 
558 }